//------------------------------------------------------------------------------
// Copyright (c) 2008 www.dnaide.com
// Licensed under the MIT (MIT-LICENSE.txt)
//------------------------------------------------------------------------------
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Web;
using System.Web.UI;
using System.Reflection;
using DNAide.Core;
using DNAide.Configuration;
using DNAide.Web.UI.Styles.Minifier;

namespace DNAide.Web.UI.Styles
{

	/// <summary>
	/// Helper class for registering / obfuscating styles
	/// </summary>
	public static class StyleAide
	{

		#region Static

		private static StyleAideContext CurrentContext
		{
			get
			{
				return (HttpContext.Current.Items["DNAideStyleAideContext"] ?? (
						HttpContext.Current.Items["DNAideStyleAideContext"] =
						new StyleAideContext(HttpContext.Current.CurrentHandler as Page))) as StyleAideContext;
			}
		}

		internal static string CacheUrl
		{
			get
			{
				return DNAide.Configuration.DNAideConfigurationSettings.Web.CacheUrl + "/Styles";
			}
		}

		internal static string EnsureCachePhysicalLocationExists()
		{
			string retval = Path.Combine(DNAide.Configuration.DNAideConfigurationSettings.Web.CachePhysicalPath, "Styles");
			if (!Directory.Exists(retval))
			{
				Directory.CreateDirectory(retval);
			}
			return retval;
		}


		/// <summary>
		/// Default register location type. By default in the &lt;head&gt; of the document
		/// </summary>
		public static StyleRegisterLocationType DefaultRegisterLocationType
		{
			get { return StyleAide.CurrentContext.DefaultRegisterLocationType; }
			set { StyleAide.CurrentContext.DefaultRegisterLocationType = value; }
		}


		/// <summary>
		/// Default control if LocationType = Control
		/// </summary>
		public static Control DefaultControl
		{
			get { return StyleAide.CurrentContext.DefaultControl; }
			set { StyleAide.CurrentContext.DefaultControl = value; }
		}


		/// <summary>
		/// Default obfuscate type. Default = None
		/// </summary>
		public static StyleObfuscateType DefaultObfuscateType
		{
			get { return StyleAide.CurrentContext.DefaultObfuscateType; }
			set { StyleAide.CurrentContext.DefaultObfuscateType = value; }
		}


		/// <summary>
		/// Whether to batch includes into one file by default. Default = False
		/// </summary>
		public static Boolean DefaultBatchIncludes
		{
			get { return StyleAide.CurrentContext.BatchIncludes; }
			set { StyleAide.CurrentContext.BatchIncludes = value; }
		}

		/// <summary>
		/// Whether to return includes as a gzip/deflate response
		/// </summary>
		public static Boolean DefaultCompressIncludes
		{
			get { return StyleAide.CurrentContext.UseCompressionHandler; }
			set { StyleAide.CurrentContext.UseCompressionHandler = value; }
		}


		/// <summary>
		/// Default media. Default = "screen,projection"
		/// </summary>
		public static String DefaultMedia
		{
			get { return StyleAide.CurrentContext.DefaultMedia; }
			set { StyleAide.CurrentContext.DefaultMedia = value; }
		}


		/// <summary>
		/// Register style details
		/// </summary>
		/// <param name="details"></param>
		public static void Register(StyleDetails details)
		{
			StyleAide.CurrentContext.AddStylesToRegister(details);
		}


		/// <summary>
		/// Register embeded styles using default values
		/// </summary>
		/// <param name="styles"></param>
		public static void RegisterEmbed(String styles)
		{
			RegisterEmbed(styles, StyleAide.DefaultRegisterLocationType, StyleAide.DefaultControl);
		}


		/// <summary>
		/// Register embedded styles in a specified control
		/// </summary>
		/// <param name="styles"></param>
		/// <param name="control"></param>
		public static void RegisterEmbed(String styles, Control control)
		{
			RegisterEmbed(styles, StyleRegisterLocationType.Control, control);
		}


		/// <summary>
		/// Register embedded styles at a specified location
		/// </summary>
		/// <param name="styles"></param>
		/// <param name="registerLocationType"></param>
		public static void RegisterEmbed(String styles, StyleRegisterLocationType registerLocationType)
		{
			RegisterEmbed(styles, registerLocationType, StyleAide.DefaultControl);
		}


		/// <summary>
		/// Register embedded styes at a specified location
		/// </summary>
		/// <param name="styles"></param>
		/// <param name="registerLocationType"></param>
		/// <param name="control"></param>
		public static void RegisterEmbed(String styles, StyleRegisterLocationType registerLocationType, Control control)
		{
			RegisterEmbed(styles, registerLocationType, control, new Dictionary<Regex, string>());
		}


		/// <summary>
		/// Register embedded styes at a specified location
		/// </summary>
		/// <param name="styles"></param>
		/// <param name="registerLocationType"></param>
		/// <param name="control"></param>
		/// <param name="replacementRules"></param>
		public static void RegisterEmbed(String styles, StyleRegisterLocationType registerLocationType, Control control, Dictionary<Regex, String> replacementRules)
		{
			StyleDetails details = new StyleDetails();
			details.StyleType = StyleType.Embed;
			details.RegisterLocation = registerLocationType;
			details.Control = control;
			details.Key = StringAide.ToHash("DNAideStyleAideEmbed" + styles, HashAlgorithmType.MD5);
			details.Value = styles;
			details.ObfuscateType = StyleAide.DefaultObfuscateType;
			details.Media = StyleAide.DefaultMedia;
			details.ReplacementRules = replacementRules;
			StyleAide.CurrentContext.AddStylesToRegister(details);
		}


		/// <summary>
		/// Register include styles using default values.
		/// </summary>
		/// <param name="stylesUrl"></param>
		public static void RegisterInclude(String stylesUrl)
		{
			RegisterInclude(stylesUrl, StyleAide.DefaultRegisterLocationType, StyleAide.DefaultControl);
		}


		/// <summary>
		/// Register include styles in a specified control
		/// </summary>
		/// <param name="stylesUrl"></param>
		/// <param name="control"></param>
		public static void RegisterInclude(String stylesUrl, Control control)
		{
			RegisterInclude(stylesUrl, StyleRegisterLocationType.Control, control);
		}


		/// <summary>
		/// Register include styles at a specified location
		/// </summary>
		/// <param name="stylesUrl"></param>
		/// <param name="registerLocationType"></param>
		public static void RegisterInclude(String stylesUrl, StyleRegisterLocationType registerLocationType)
		{
			RegisterInclude(stylesUrl, registerLocationType, StyleAide.DefaultControl);
		}


		/// <summary>
		/// Register include styles at a specified location
		/// </summary>
		/// <param name="stylesUrl"></param>
		/// <param name="registerLocationType"></param>
		/// <param name="control"></param>
		public static void RegisterInclude(String stylesUrl, StyleRegisterLocationType registerLocationType, Control control)
		{
			RegisterInclude(stylesUrl, registerLocationType, control, new Dictionary<Regex, string>());
		}


		/// <summary>
		/// Register include styles at a specified location
		/// </summary>
		/// <param name="stylesUrl"></param>
		/// <param name="registerLocationType"></param>
		/// <param name="control"></param>
		/// <param name="replacementRules"></param>
		public static void RegisterInclude(String stylesUrl, StyleRegisterLocationType registerLocationType, Control control, Dictionary<Regex, String> replacementRules)
		{
			StyleDetails details = new StyleDetails();
			details.StyleType = StyleType.Include;
			details.RegisterLocation = registerLocationType;
			details.Control = control;
			details.Key = StringAide.ToHash("DNAideStyleAideInclude" + stylesUrl, HashAlgorithmType.MD5); ;
			details.Value = stylesUrl;
			details.ObfuscateType = StyleAide.DefaultObfuscateType;
			details.Media = StyleAide.DefaultMedia;
			details.ReplacementRules = replacementRules;
			StyleAide.CurrentContext.AddStylesToRegister(details);
		}


		/// <summary>
		/// Register include styles using default values.
		/// Style will not be copied to the cache. No ObfuscateType will take place.
		/// </summary>
		/// <param name="stylesUrl"></param>
		public static void RegisterIncludeNoCache(String stylesUrl)
		{
			RegisterIncludeNoCache(stylesUrl, StyleAide.DefaultRegisterLocationType, StyleAide.DefaultControl);
		}


		/// <summary>
		/// Register include styles in a specified control
		/// Style will not be copied to the cache. No ObfuscateType will take place.
		/// </summary>
		/// <param name="stylesUrl"></param>
		/// <param name="control"></param>
		public static void RegisterIncludeNoCache(String stylesUrl, Control control)
		{
			RegisterIncludeNoCache(stylesUrl, StyleRegisterLocationType.Control, control);
		}


		/// <summary>
		/// Register include styles at a specified location
		/// Style will not be copied to the cache. No ObfuscateType will take place.
		/// </summary>
		/// <param name="stylesUrl"></param>
		/// <param name="registerLocationType"></param>
		public static void RegisterIncludeNoCache(String stylesUrl, StyleRegisterLocationType registerLocationType)
		{
			RegisterIncludeNoCache(stylesUrl, registerLocationType, StyleAide.DefaultControl);
		}


		/// <summary>
		/// Register include styles at a specified location
		/// Style will not be copied to the cache. No ObfuscateType will take place.
		/// </summary>
		/// <param name="stylesUrl"></param>
		/// <param name="registerLocationType"></param>
		/// <param name="control"></param>
		public static void RegisterIncludeNoCache(String stylesUrl, StyleRegisterLocationType registerLocationType, Control control)
		{
			RegisterIncludeNoCache(stylesUrl, registerLocationType, control, new Dictionary<Regex, string>());
		}


		/// <summary>
		/// Register include styles at a specified location
		/// Style will not be copied to the cache. No ObfuscateType will take place.
		/// </summary>
		/// <param name="stylesUrl"></param>
		/// <param name="registerLocationType"></param>
		/// <param name="control"></param>
		/// <param name="replacementRules"></param>
		public static void RegisterIncludeNoCache(String stylesUrl, StyleRegisterLocationType registerLocationType, Control control, Dictionary<Regex, String> replacementRules)
		{
			StyleDetails details = new StyleDetails();
			details.StyleType = StyleType.IncludeNoCache;
			details.RegisterLocation = registerLocationType;
			details.Control = control;
			details.Key = StringAide.ToHash("DNAideStyleAideInclude" + stylesUrl, HashAlgorithmType.MD5); ;
			details.Value = stylesUrl;
			details.ObfuscateType = StyleAide.DefaultObfuscateType;
			details.Media = StyleAide.DefaultMedia;
			details.ReplacementRules = replacementRules;
			StyleAide.CurrentContext.AddStylesToRegister(details);
		}


		internal static String Minify(string styles)
		{
			StyleMinifier minifier = new StyleMinifier();
			return minifier.Minify(styles);
		}


		/// <summary>
		/// Obfuscate styles
		/// </summary>
		/// <param name="styles"></param>
		/// <param name="obfuscateType"></param>
		/// <returns></returns>
		public static String Obfuscate(string styles, StyleObfuscateType obfuscateType)
		{
			styles = CastAide.AsString(styles);
			switch (obfuscateType)
			{
				case StyleObfuscateType.Minify:
					styles = StyleAide.Minify(styles);
					break;
			}
			return styles;
		}

		#endregion

	}


	internal class StyleAideContext
	{
		private object _lock = new object();
		private Page _page;
		private StyleObfuscateType _defaultObfuscateType;
		private StyleRegisterLocationType _defaultRegisterLocationType;
		private string _defaultMedia;
		private Control _defaultControl;
		private bool _batchIncludes;
		private bool _useCompressionHandler;
		private List<string> _uniqueKeys;
		private Dictionary<Control, Dictionary<string, StyleDetails>> _stylesToRegister;

		internal StyleObfuscateType DefaultObfuscateType
		{
			get { return this._defaultObfuscateType; }
			set { this._defaultObfuscateType = value; }
		}

		internal StyleRegisterLocationType DefaultRegisterLocationType
		{
			get { return this._defaultRegisterLocationType; }
			set { this._defaultRegisterLocationType = value; }
		}

		internal Control DefaultControl
		{
			get { return this._defaultControl; }
			set { this._defaultControl = value; }
		}

		internal bool BatchIncludes
		{
			get { return this._batchIncludes; }
			set { this._batchIncludes = value; }
		}

		internal bool UseCompressionHandler
		{
			get { return this._useCompressionHandler; }
			set { this._useCompressionHandler = value; }
		}

		internal string DefaultMedia
		{
			get { return this._defaultMedia; }
			set { this._defaultMedia = value; }
		}

		internal StyleAideContext(Page page)
		{
			this._page = page;
			this._page.PreRenderComplete += new EventHandler(_page_PreRenderComplete);

			this._defaultObfuscateType = DNAideConfigurationSettings.Web.StyleAide.DefaultObfuscateType;
			this._defaultRegisterLocationType = DNAideConfigurationSettings.Web.StyleAide.DefaultRegisterLocationType;

			if (!string.IsNullOrEmpty(DNAideConfigurationSettings.Web.StyleAide.DefaultControl))
			{
				this._defaultControl = PageAide.FindControl<Control>(this._page, DNAideConfigurationSettings.Web.StyleAide.DefaultControl);
			}

			this._batchIncludes = DNAideConfigurationSettings.Web.StyleAide.BatchIncludes;
			this._useCompressionHandler = DNAideConfigurationSettings.Web.StyleAide.UseCompressionHandler;
			this._defaultMedia = DNAideConfigurationSettings.Web.StyleAide.DefaultMedia;
			this._stylesToRegister = new Dictionary<Control, Dictionary<string, StyleDetails>>();
			this._uniqueKeys = new List<string>();
		}

		void _page_PreRenderComplete(object sender, EventArgs e)
		{
			foreach (Control registerControl in this._stylesToRegister.Keys)
			{
				StringBuilder sb = new StringBuilder();
				StringBuilder embedSb = new StringBuilder();

				Dictionary<string, BatchDetails> toBatch = new Dictionary<string, BatchDetails>();

				foreach (KeyValuePair<string, StyleDetails> kvp in this._stylesToRegister[registerControl])
				{
					string cacheUrlFileName, cachePhysicalFileName, cssText;

					switch (kvp.Value.StyleType)
					{
						
						case StyleType.Include:
							// Register include
							// Take a copy and move it into the cache if applicable
							string originalPhysicalFileName = HttpContext.Current.Server.MapPath(kvp.Value.Value);
							if (!File.Exists(originalPhysicalFileName))
							{
								throw new Exception(string.Format(Resources.StyleAide.UnableToLocateInclude, originalPhysicalFileName));
							}

							cacheUrlFileName = string.Format("{0}/{1}.{2}.css", StyleAide.CacheUrl, Path.GetFileName(originalPhysicalFileName), StringAide.ToHash(kvp.Value.Value + File.GetLastWriteTime(originalPhysicalFileName).Ticks.ToString(), HashAlgorithmType.MD5));
							cachePhysicalFileName = HttpContext.Current.Server.MapPath(cacheUrlFileName);

							// Create the physical file in the cache
							if (!File.Exists(cachePhysicalFileName))
							{
								lock (this._lock)
								{
									if (!File.Exists(cachePhysicalFileName))
									{
										StyleAide.EnsureCachePhysicalLocationExists();
										cssText = File.ReadAllText(originalPhysicalFileName);
										cssText = ApplyGlobalReplacementRules(cssText);
										cssText = ApplyReplacementRules(kvp.Value.ReplacementRules, cssText);
										File.WriteAllText(cachePhysicalFileName, StyleAide.Obfuscate(cssText, kvp.Value.ObfuscateType));
									}
								}
							}

							if (this.BatchIncludes)
							{
								if (!toBatch.ContainsKey(kvp.Value.Media))
								{
									toBatch[kvp.Value.Media] = new BatchDetails();
								}
								toBatch[kvp.Value.Media].FilesToBatch.Add(cachePhysicalFileName);
								toBatch[kvp.Value.Media].BatchFileName.Append(cacheUrlFileName);
								toBatch[kvp.Value.Media].BatchFileName.Append(File.GetLastWriteTime(cachePhysicalFileName).Ticks);
							}
							else
							{
								sb.AppendFormat(Resources.StyleAide.IncludeMarkup, kvp.Value.Media, this._page.ResolveUrl(cacheUrlFileName));
							}
							break;

						case StyleType.IncludeNoCache:
							// Register include but do not copy to the cache nor obfuscate nor batch
							// Take a copy and move it into the cache if applicable

							sb.AppendFormat(Resources.StyleAide.IncludeMarkup, kvp.Value.Media, this._page.ResolveUrl(kvp.Value.Value));
							break;

						case StyleType.Embed:
							// Register Embed Style
							cssText = ApplyGlobalReplacementRules(kvp.Value.Value);
							cssText = ApplyReplacementRules(kvp.Value.ReplacementRules, kvp.Value.Value);

							// If batching includes, ensure all includes are registered before any embedded code
							if (this.BatchIncludes)
							{
								embedSb.AppendFormat(Resources.StyleAide.InlineMarkup, StyleAide.Obfuscate(cssText, kvp.Value.ObfuscateType));
							}
							else
							{
								sb.AppendFormat(Resources.StyleAide.InlineMarkup, StyleAide.Obfuscate(cssText, kvp.Value.ObfuscateType));
							}
							break;

					}
				}

				
				if (toBatch.Count > 0)
				{
					foreach (KeyValuePair<string, BatchDetails> batchKvp in toBatch)
					{
						string hashedBatchFileName = StringAide.ToHash(batchKvp.Value.BatchFileName.ToString(), HashAlgorithmType.MD5) + ".batched.css";
						string hashedBatchFileNameUrlPath = string.Format("{0}/{1}", StyleAide.CacheUrl, hashedBatchFileName);
						string hashedBatchFileNamePhysicalPath = HttpContext.Current.Server.MapPath(hashedBatchFileNameUrlPath);

						if (!File.Exists(hashedBatchFileNamePhysicalPath))
						{
							lock (this._lock)
							{
								if (!File.Exists(hashedBatchFileNamePhysicalPath))
								{
									StyleAide.EnsureCachePhysicalLocationExists();
									StringBuilder batchedFileContent = new StringBuilder();
									foreach (string fileToBatch in batchKvp.Value.FilesToBatch)
									{
										File.AppendAllText(hashedBatchFileNamePhysicalPath, File.ReadAllText(fileToBatch));
									}
								}
							}
						}
						if (this.UseCompressionHandler)
						{
							sb.AppendFormat(Resources.StyleAide.IncludeMarkup, batchKvp.Key, this._page.ResolveUrl("~/DNAide/StyleAide.axd?action=compress&file=" + HttpContext.Current.Server.UrlEncode(hashedBatchFileName)));
						}
						else
						{
							sb.AppendFormat(Resources.StyleAide.IncludeMarkup, batchKvp.Key, this._page.ResolveUrl(hashedBatchFileNameUrlPath));
						}
					}
				}

			
				LiteralControl lit = new LiteralControl(sb.ToString() + embedSb.ToString());
				
				registerControl.Controls.Add(lit);
			}
		}



		internal static string ApplyReplacementRules(Dictionary<Regex, string> replacementRules, string input)
		{
			string retval = input;
			foreach (KeyValuePair<Regex, string> replacementKvp in replacementRules)
			{
				retval = replacementKvp.Key.Replace(retval, replacementKvp.Value.Replace("~/", UrlAide.GetApplicationPathForwardSlashAppended()));
			}
			return retval;
		}


		internal static string ApplyGlobalReplacementRules(string input)
		{
			string retval = input;
			foreach (StyleAideReplacementRuleConfig config in DNAideConfigurationSettings.Web.StyleAide.GlobalReplacementRules)
			{
				retval = config.PatternMatchExpressionRegex.Replace(retval, config.ReplaceExpression.Replace("~/", UrlAide.GetApplicationPathForwardSlashAppended()));
			}
			return retval;
		}
	


		internal void AddStylesToRegister(StyleDetails details)
		{
			if (details == null)
			{
				throw new ArgumentException("details");
			}

			if (this._uniqueKeys.Contains(details.Key))
			{
				return;
			}

			Control registerControl;
			switch (details.RegisterLocation)
			{
				case StyleRegisterLocationType.Form:
					registerControl = this._page.Form;
					break;
				case StyleRegisterLocationType.Head:
					registerControl = this._page.Header;
					break;
				default:
					registerControl = details.Control;
					break;
			}

			if (registerControl == null)
			{
				throw new ArgumentException(Resources.StyleAide.CannotFindControl);
			}

			if (string.IsNullOrEmpty(details.Key))
			{
				throw new ArgumentException(Resources.StyleAide.KeyRequired);
			}

			if (!this._stylesToRegister.ContainsKey(registerControl))
			{
				this._stylesToRegister.Add(registerControl, new Dictionary<string, StyleDetails>());
			}
			if (!this._stylesToRegister[registerControl].ContainsKey(details.Key))
			{
				this._stylesToRegister[registerControl].Add(details.Key, details);
			}

			this._uniqueKeys.Add(details.Key);
		}


		public class BatchDetails
		{
			public StringBuilder BatchFileName;
			public List<string> FilesToBatch;

			public BatchDetails()
			{
				BatchFileName = new StringBuilder();
				FilesToBatch = new List<string>();
			}
		}

	}


	/// <summary>
	/// Class to hold style detail information for register purposes
	/// </summary>
	public class StyleDetails
	{

		private StyleType _styleType;
		private StyleRegisterLocationType _registerLocation;
		private Control _control;
		private string _key;
		private string _value;
		private string _media;
		private StyleObfuscateType _obfuscateType;
		private Dictionary<Regex, string> _replacementRules;

		/// <summary>
		/// StyleType
		/// </summary>
		public StyleType StyleType
		{
			get { return this._styleType; }
			set { this._styleType = value; }
		}

		/// <summary>
		/// Where in the page the style is to be registered
		/// </summary>
		public StyleRegisterLocationType RegisterLocation
		{
			get { return this._registerLocation; }
			set { this._registerLocation = value; }
		}

		/// <summary>
		/// If the style is to be regsitered in a control
		/// </summary>
		public Control Control
		{
			get { return this._control; }
			set { this._control = value; }
		}

		/// <summary>
		/// Key identifier
		/// </summary>
		public String Key
		{
			get { return this._key; }
			set { this._key = value; }
		}

		/// <summary>
		/// Value, either the style or url
		/// </summary>
		public String Value
		{
			get { return this._value; }
			set { this._value = value; }
		}

		/// <summary>
		/// Media (comma separated string - all | aural | braille | embossed | handheld | print | projection | screen | tty | tv
		/// </summary>
		public String Media
		{
			get { return this._media; }
			set { this._media = value; }
		}

		/// <summary>
		/// The obfuscate method to use
		/// </summary>
		public StyleObfuscateType ObfuscateType
		{
			get { return this._obfuscateType; }
			set { this._obfuscateType = value; }
		}

		public Dictionary<Regex, string> ReplacementRules
		{
			get
			{
				if (this._replacementRules == null)
				{
					this._replacementRules = new Dictionary<Regex, string>();
				}
				return this._replacementRules;
			}
			set
			{
				this._replacementRules = value;
			}
		}

		/// <summary></summary>
		public StyleDetails()
		{
		}

	}

}
